package com.magenta.maxoptra.scheduler.io;

import java.io.*;
import java.util.Arrays;
import java.util.regex.Pattern;

/**
 * Maxoptra 1.2
 * User: ivictus@rambler.ru
 * Date: 02.03.2010
 * Time: 10:54:32
 * <p/>
 * Copyright (c) 1999-2010 Magenta Corporation Ltd. All Rights Reserved.
 * Magenta Technology proprietary and confidential.
 * Use is subject to license terms.
 */

/**
 * Extends {@link PrintWriter} class.<br>
 * Supports three basic CSV format parameters:<br>
 * <li>lineSeparator - by default {@link #LINE_SEPARATOR}</li>
 * <li>quote - by default {@link #QUOTE}</li>
 * <li>valueSeparator - by default {@link #VALUE_SEPARATOR}</li>
 * <br> All values write by methods print placed as quoted <code>String</code> if need and separated by <code>valueSeparator</code>.
 */
public class CSVWriter extends PrintWriter {

    private boolean autoFlush;

    private boolean nextValue;

    /**
     * CSV format parameters.
     */
    private final char[] lineSeparator;
    private final char[] quote;
    private final char[] valueSeparator;

    /**
     * Default parameters values.
     */
    public static final char[] LINE_SEPARATOR = System.getProperty("line.separator", "\n").toCharArray();
    public static final char[] QUOTE = {'"'};
    public static final char[] VALUE_SEPARATOR = {','};

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.Writer)}.
     * @param out A character-output stream
     * @param lineSeparator Line separator if null use default {@link #LINE_SEPARATOR}
     * @param quote if quote is null then use default {@link #QUOTE}
     * @param valueSeparator Value separator if null use default {@link #VALUE_SEPARATOR}
     */
    public CSVWriter(Writer out, char[] lineSeparator, char[] quote, char[] valueSeparator) {
        super(out);
        this.lineSeparator = ifNull(lineSeparator, LINE_SEPARATOR);
        this.quote = ifNull(quote, QUOTE);
        this.valueSeparator = ifNull(valueSeparator, VALUE_SEPARATOR);
        initQuotes();
    }

    private char[] ifNull(char[] value, char[] defaultValue) {
        return value != null && value.length > 0 ? value: defaultValue;
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.Writer, boolean)}.
     * @param out A character-output stream
     * @param autoFlush A boolean; if true, the println, printf, or format methods will flush the output buffer
     * @param lineSeparator Line separator if null use default {@link #LINE_SEPARATOR}
     * @param quote if quote is null then use default {@link #QUOTE}
     * @param valueSeparator Value separator if null use default {@link #VALUE_SEPARATOR}
     */
    public CSVWriter(Writer out, boolean autoFlush, char[] lineSeparator, char[] quote, char[] valueSeparator) {
        super(out, autoFlush);
        this.lineSeparator = ifNull(lineSeparator, LINE_SEPARATOR);
        this.quote = ifNull(quote, QUOTE);
        this.valueSeparator = ifNull(valueSeparator, VALUE_SEPARATOR);
        this.autoFlush = autoFlush;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.OutputStream)}.
     * @param out An output stream
     * @param lineSeparator Line separator if null use default {@link #LINE_SEPARATOR}
     * @param quote if quote is null then use default {@link #QUOTE}
     * @param valueSeparator Value separator if null use default {@link #VALUE_SEPARATOR}
     */
    public CSVWriter(OutputStream out, char[] lineSeparator, char[] quote, char[] valueSeparator) {
        super(out);
        this.lineSeparator = ifNull(lineSeparator, LINE_SEPARATOR);
        this.quote = ifNull(quote, QUOTE);
        this.valueSeparator = ifNull(valueSeparator, VALUE_SEPARATOR);
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.OutputStream, boolean)}.
     * @param out An output stream
     * @param autoFlush A boolean; if true, the println, printf, or format methods will flush the output buffer
     * @param lineSeparator Line separator if null use default {@link #LINE_SEPARATOR}
     * @param quote if quote is null then use default {@link #QUOTE}
     * @param valueSeparator Value separator if null use default {@link #VALUE_SEPARATOR}
     */
    public CSVWriter(OutputStream out, boolean autoFlush, char[] lineSeparator, char[] quote, char[] valueSeparator) {
        super(out, autoFlush);
        this.lineSeparator = ifNull(lineSeparator, LINE_SEPARATOR);
        this.quote = ifNull(quote, QUOTE);
        this.valueSeparator = ifNull(valueSeparator, VALUE_SEPARATOR);
        this.autoFlush = autoFlush;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.Writer)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param out A character-output stream
     */
    public CSVWriter(Writer out) {
        super(out);
        this.lineSeparator = LINE_SEPARATOR;
        this.quote = QUOTE;
        this.valueSeparator = VALUE_SEPARATOR;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.Writer, boolean)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param out A character-output stream
     * @param autoFlush A boolean; if true, the println, printf, or format methods will flush the output buffer
     */
    public CSVWriter(Writer out, boolean autoFlush) {
        super(out, autoFlush);
        this.lineSeparator = LINE_SEPARATOR;
        this.quote = QUOTE;
        this.valueSeparator = VALUE_SEPARATOR;
        this.autoFlush = autoFlush;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.OutputStream)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param out An output stream
     */
    public CSVWriter(OutputStream out) {
        super(out);
        this.lineSeparator = LINE_SEPARATOR;
        this.quote = QUOTE;
        this.valueSeparator = VALUE_SEPARATOR;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.OutputStream, boolean)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param out An output stream
     * @param autoFlush A boolean; if true, the println, printf, or format methods will flush the output buffer
     */
    public CSVWriter(OutputStream out, boolean autoFlush) {
        super(out, autoFlush);
        this.lineSeparator = LINE_SEPARATOR;
        this.quote = QUOTE;
        this.valueSeparator = VALUE_SEPARATOR;
        this.autoFlush = autoFlush;
        initQuotes();
    }

    protected void ensureOpen() throws IOException {
        if (out == null) {
           throw new IOException("Stream closed");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void println() {
        try {
            synchronized (lock) {
                ensureOpen();

                out.write(lineSeparator);

                if (autoFlush) {
                   out.flush();
                }

                nextValue = false;
            }
        }
        catch (InterruptedIOException e) {
           Thread.currentThread().interrupt();
        }
        catch (IOException e) {
           setError();
        }
    }

    private String singleQuote;
    private String doubleQuote;
    private Pattern itIsWord = Pattern.compile("\\w*");

    private void initQuotes() {
        singleQuote = String.valueOf(quote);
        doubleQuote = singleQuote + singleQuote;
    }
    /**
     * {@inheritDoc}
     * String always be quoted if contains non character symbols.
     */
    @Override
    public void write(String s, int off, int len) {
        try {
            synchronized (lock) {
                ensureOpen();
                if (nextValue) {
                    out.write(valueSeparator);
                }
                boolean quoting = !itIsWord.matcher(s).matches();
                if (quoting) {
                    out.write(quote);
                    String unquotingString = s.substring(off, len);
                    out.write(unquotingString.replaceAll(singleQuote, doubleQuote));
                    out.write(quote);
                }
                else {
                    out.write(s, off, len);
                }
                nextValue = true;
            }
        }
        catch (InterruptedIOException e) {
            Thread.currentThread().interrupt();
        }
        catch (IOException e) {
            setError();
        }
    }

    /**
     * {@inheritDoc}<br>
     * If chars contains quoted char-sequence then replace on double quoting.
     */
    @Override
    public void write(char[] buf, int off, int len) {
        try {
            synchronized (lock) {
                ensureOpen();
                if (nextValue) {
                    out.write(valueSeparator);
                }
                boolean quoting = !itIsWord.matcher(String.valueOf(buf)).matches();
                if (quoting) {
                    out.write(quote);
                    char[] unquoting = Arrays.copyOfRange(buf, off, len);
                    out.write(String.valueOf(unquoting).replaceAll(singleQuote, doubleQuote).toCharArray());
                    out.write(quote);
                }
                else {
                    out.write(buf, off, len);
                }
                nextValue = true;
            }
        }
        catch (InterruptedIOException e) {
            Thread.currentThread().interrupt();
        }
        catch (IOException e) {
            setError();
        }
    }

    //=====================================================================
    // OVERRIDE CONSTRUCTORS
    //=====================================================================

    /**
     * Same as {@link PrintWriter#PrintWriter(String)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param fileName The name of the file to use as the destination of this writer. If the file exists then it will be truncated to zero size; otherwise, a new file will be created. The output will be written to the file and is buffered.
     * @throws FileNotFoundException If the given string does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file
     */
    public CSVWriter(String fileName) throws FileNotFoundException {
        super(fileName);
        this.lineSeparator = LINE_SEPARATOR;
        this.quote = QUOTE;
        this.valueSeparator = VALUE_SEPARATOR;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(String)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param fileName The name of the file to use as the destination of this writer. If the file exists then it will be truncated to zero size; otherwise, a new file will be created. The output will be written to the file and is buffered.
     * @param lineSeparator Line separator if null by default {@link #LINE_SEPARATOR}
     * @param quote if quote is null then by default {@link #QUOTE}
     * @param valueSeparator Value separator if null by default {@link #VALUE_SEPARATOR}
     * @throws FileNotFoundException If the given string does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file
     */
    public CSVWriter(String fileName, char[] lineSeparator, char[] quote, char[] valueSeparator) throws FileNotFoundException {
        super(fileName);
        this.lineSeparator = ifNull(lineSeparator, LINE_SEPARATOR);
        this.quote = ifNull(quote, QUOTE);
        this.valueSeparator = ifNull(valueSeparator, VALUE_SEPARATOR);
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(String, String)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param fileName The name of the file to use as the destination of this writer. If the file exists then it will be truncated to zero size; otherwise, a new file will be created. The output will be written to the file and is buffered.
     * @param csn The name of a supported {@linkplain java.nio.charset.Charset charset}
     * @param lineSeparator Line separator if null use default {@link #LINE_SEPARATOR}
     * @param quote if quote is null then use default {@link #QUOTE}
     * @param valueSeparator Value separator if null use default {@link #VALUE_SEPARATOR}
     * @throws FileNotFoundException If the given string does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file
     * @throws UnsupportedEncodingException If the named charset is not supported
     */
    public CSVWriter(String fileName, String csn, char[] lineSeparator, char[] quote, char[] valueSeparator) throws FileNotFoundException, UnsupportedEncodingException {
        super(fileName, csn);
        this.lineSeparator = ifNull(lineSeparator, LINE_SEPARATOR);
        this.quote = ifNull(quote, QUOTE);
        this.valueSeparator = ifNull(valueSeparator, VALUE_SEPARATOR);
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(String, String)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param fileName The name of the file to use as the destination of this writer. If the file exists then it will be truncated to zero size; otherwise, a new file will be created. The output will be written to the file and is buffered.
     * @param csn The name of a supported {@linkplain java.nio.charset.Charset charset}
     * @throws FileNotFoundException If the given string does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file
     * @throws UnsupportedEncodingException If the named charset is not supported
     */
    public CSVWriter(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException {
        super(fileName, csn);
        this.lineSeparator = LINE_SEPARATOR;
        this.quote = QUOTE;
        this.valueSeparator = VALUE_SEPARATOR;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(File)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param lineSeparator Line separator if null use default {@link #LINE_SEPARATOR}
     * @param quote if quote is null then use default {@link #QUOTE}
     * @param valueSeparator Value separator if null use default {@link #VALUE_SEPARATOR}
     * @param file The file to use as the destination of this writer. If the file exists then it will be truncated to zero size; otherwise, a new file will be created. The output will be written to the file and is buffered.
     * @throws FileNotFoundException If the given string does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file
     */
    public CSVWriter(File file, char[] lineSeparator, char[] quote, char[] valueSeparator) throws FileNotFoundException {
        super(file);
        this.lineSeparator = ifNull(lineSeparator, LINE_SEPARATOR);
        this.quote = ifNull(quote, QUOTE);
        this.valueSeparator = ifNull(valueSeparator, VALUE_SEPARATOR);
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(File)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param file The file to use as the destination of this writer. If the file exists then it will be truncated to zero size; otherwise, a new file will be created. The output will be written to the file and is buffered.
     * @throws FileNotFoundException If the given string does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file
     */
    public CSVWriter(File file) throws FileNotFoundException {
        super(file);
        this.lineSeparator = LINE_SEPARATOR;
        this.quote = QUOTE;
        this.valueSeparator = VALUE_SEPARATOR;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.File, String)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param file The file to use as the destination of this writer. If the file exists then it will be truncated to zero size; otherwise, a new file will be created. The output will be written to the file and is buffered.
     * @param csn The name of a supported {@linkplain java.nio.charset.Charset charset}
     * @throws FileNotFoundException If the given string does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file
     * @throws UnsupportedEncodingException If the named charset is not supported
     */
    public CSVWriter(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException {
        super(file, csn);
        this.lineSeparator = LINE_SEPARATOR;
        this.quote = QUOTE;
        this.valueSeparator = VALUE_SEPARATOR;
        initQuotes();
    }

    /**
     * Same as {@link PrintWriter#PrintWriter(java.io.File, String)}.<br>
     * CSV format parameters usage by default: {@link #LINE_SEPARATOR}, {@link #QUOTE}, {@link #VALUE_SEPARATOR}.
     * @param file The file to use as the destination of this writer. If the file exists then it will be truncated to zero size; otherwise, a new file will be created. The output will be written to the file and is buffered.
     * @param csn The name of a supported {@linkplain java.nio.charset.Charset charset}
     * @param lineSeparator Line separator if null use default {@link #LINE_SEPARATOR}
     * @param quote if quote is null then use default {@link #QUOTE}
     * @param valueSeparator Value separator if null use default {@link #VALUE_SEPARATOR}
     * @throws FileNotFoundException If the given string does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file
     * @throws UnsupportedEncodingException If the named charset is not supported
     */
    public CSVWriter(File file, String csn, char[] lineSeparator, char[] quote, char[] valueSeparator) throws FileNotFoundException, UnsupportedEncodingException {
        super(file, csn);
        this.lineSeparator = ifNull(lineSeparator, LINE_SEPARATOR);
        this.quote = ifNull(quote, QUOTE);
        this.valueSeparator = ifNull(valueSeparator, VALUE_SEPARATOR);
        initQuotes();
    }
}
